// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.metrics;

import com.yahoo.text.XMLWriter;

import java.util.*;
import java.util.logging.Logger;

public abstract class MetricSet extends Metric
{
    private static Logger log = Logger.getLogger(MetricSet.class.getName());

    List<Metric> metricOrder = new ArrayList<Metric>();    // Keep added order for reporting
    boolean registrationAltered; // Set to true if metrics have been
                                 // registered/unregistered since last time
                                 // it was reset

    public MetricSet(String name, String tags, String description, MetricSet owner) {
        super(name, tags, description, owner);
    }

    public MetricSet(MetricSet other, CopyType copyType, MetricSet owner, boolean includeUnused) {
        super(other, owner);

        for (Metric m : other.metricOrder) {
            if (copyType != CopyType.INACTIVE || includeUnused || m.used()) {
                m.clone(copyType, this, includeUnused);
            }
        }
    }

    /**
     * @return Returns true if registration has been altered since it was last
     * cleared. Used by the metric manager to know when it needs to recalculate
     * which consumers will see what.
     */
    public boolean isRegistrationAltered() { return registrationAltered; }

    /** Clear all registration altered flags. */
    void clearRegistrationAltered() {
        visit(new MetricVisitor() {
            public boolean visitMetricSet(MetricSet set, boolean autoGenerated) {
                if (autoGenerated) {
                    return false;
                }

                set.registrationAltered = false;
                return true;
            }
        }, false);
    }

    public void registerMetric(Metric m) {
        if (m.isRegistered()) {
            throw new IllegalStateException("Metric " + m.getName() +
                    " is already registered in a metric set. Cannot register it twice.");
        }

        if (getMetricInternal(m.getName()) != null) {
            throw new IllegalStateException("A metric named " + m.getName() + " is already registered "
                + "in metric set " + getPath());
        }

        metricOrder.add(m);
        m.owner = this;
        tagRegistrationAltered();
    }

    public void unregisterMetric(Metric m) {
        // In case of abrubt shutdowns, don't die hard on attempts to unregister
        // non-registered metrics. Just warn and ignore.
        if (!metricOrder.remove(m)) {
            log.warning("Attempt to unregister metric " + m.getName() + " in metric set " + getPath() +
                    ", where it wasn't registered to begin with.");
            return;
        }

        m.owner = null;
        tagRegistrationAltered();

        log.finest("Unregistered metric " + m.getName() + " from metric set " + getPath() + ".");
    }

    @Override
    public void reset() {
        for (Metric m : metricOrder) {
            m.reset();
        }
    }

    @Override
    public boolean visit(MetricVisitor visitor, boolean tagAsAutoGenerated) {
        if (!visitor.visitMetricSet(this, tagAsAutoGenerated)) {
            return true;
        }

        for (Metric m : metricOrder) {
            if (!m.visit(visitor, tagAsAutoGenerated)) {
                break;
            }
        }

        visitor.doneVisitingMetricSet(this);
        return true;
    }

    @Override
    public void logEvent(EventLogger logger, String fullName) {
        throw new IllegalStateException("Can't log event from a MetricsSet: " + fullName);
    }

        // These should never be called on metrics set.
    @Override
    public long getLongValue(String id) {
        throw new IllegalStateException("Tried to get long from metricset");
    }

    @Override
    public double getDoubleValue(String id) {
        throw new IllegalStateException("Tried to get double from metricset");
    }

    public Metric getMetric(String name) {
        int pos = name.indexOf('.');
        if (pos == -1) {
            return getMetricInternal(name);
        } else {
            String child = name.substring(0, pos);
            String rest = name.substring(pos + 1);

            Metric m = getMetricInternal(child);
            if (m == null || !(m instanceof MetricSet)) {
                return null;
            } else {
                return ((MetricSet)m).getMetric(rest);
            }
        }
    }

    private Metric getMetricInternal(String name) {
        for (Metric m : metricOrder) {
            if (m.getName().equals(name)) {
                return m;
            }
        }
        return null;
    }

    Map<String, Metric> createMetricMap() {
        Map<String, Metric> map = new TreeMap<String, Metric>();

        for (Metric m : metricOrder) {
            map.put(m.getName(), m);
        }

        return map;
    }

    @Override
    public void addToSnapshot(Metric snapshotMetric) {
        MetricSet o = (MetricSet)snapshotMetric;

        Map<String, Metric> map1 = createMetricMap();
        Set<String> seen = new HashSet<String>();

        // For all the metrics in the other's order, join ours to the snapshot.
        for (Metric m : o.metricOrder) {
            Metric myCopy = map1.get(m.getName());

            if (myCopy != null) {
                seen.add(m.getName());
                myCopy.addToSnapshot(m);
            }
        }

        // For all the remaining metrics, just join them to the other one.
        for (Metric m : metricOrder) {
            if (!seen.contains(m.getName())) {
                m.clone(CopyType.INACTIVE, o, false);
            }
        }
    }

    List<Metric> getRegisteredMetrics()
        { return metricOrder; }

    @Override
    public boolean used() {
        for (Metric m : metricOrder) {
            if (m.used()) {
                return true;
            }
        }

        return false;
    }

    @Override
    public void addToPart(Metric partMetric) {
        MetricSet o = (MetricSet)partMetric;

        Map<String, Metric> map2 = o.createMetricMap();

        for (Metric m : metricOrder) {
            Metric other = map2.get(m.getName());
            if (other != null) {
                m.addToPart(other);
            } else {
                m.clone(CopyType.INACTIVE, o, false);
            }
        }
    }

    private void tagRegistrationAltered() {
        registrationAltered = true;
        if (owner != null) {
            owner.tagRegistrationAltered();
        }
    }

    @Override
    public void printXml(XMLWriter writer,
                         int secondsPassed, int verbosity)
    {
        if (!used() && verbosity < 3) {
            return;
        }

        openXMLTag(writer, verbosity);

        for (Metric m : metricOrder) {
            m.printXml(writer, secondsPassed, verbosity);
        }

        writer.closeTag();
    }


}
